About
Relay Rhythms is an interactive sound sculpture that uses salvaged industrial relays and custom circuitry to create polyrhythmic clicks. Clicking is controlled by the black knobs, and corresponding lights illuminate when each click occurs. The device subdivides a master tempo into integer divisions between a whole note and sixteenth note (i.e. 1:1 through 1:16) and consequently maintains locked rhythmic relationships between the clicks. Since there are three relays, polyrhythms like 1:2:3 or 13:14:15 are available, and master tempo is controlled by the black knob on the front of the box. The circuitry also has three audio jacks which output signals that toggle between zero and five volts in synchronization with corresponding relays. Relay timing and control is managed by an arduino which sends signals to transistors that energize the relays.

A video of Relay Rhythms being used to make music is available here.

Relay Rhythms was used in performances during the 2013 California Electronic Music Exchange Concerts at UCSB, Mills, and CalArts, and was also used at a 2013 graduate computer music concert at UCSD.

Photos
Circuitry
Relay Rhythms Code
//------------------------------------------------------------------------------
//  main.ino
//  Relay Rhythms - Arduino
//
//  Created by Cooper Baker on 3/23/12.
//  Copyright (c) 2012 Cooper Baker. All rights reserved.
//------------------------------------------------------------------------------

#include "main.h"


//------------------------------------------------------------------------------
// definitions
//------------------------------------------------------------------------------
#define RELAY_PIN_LEFT   2
#define RELAY_PIN_CENTER 3
#define RELAY_PIN_RIGHT  4

#define LED_PIN_LEFT     10
#define LED_PIN_CENTER   12
#define LED_PIN_RIGHT    11

#define LED_PIN_FRONT    9

#define AUDIO_PIN_LEFT   8
#define AUDIO_PIN_CENTER 7
#define AUDIO_PIN_RIGHT  13

#define POT_PIN_LEFT     3
#define POT_PIN_CENTER   2
#define POT_PIN_RIGHT    1
#define POT_PIN_FRONT    0

#define SWITCH_PIN_A     6
#define SWITCH_PIN_B     5

#define NUM_TICKS        17

#define AUDIO_PULSE_USEC 10
#define LED_TICKS        8

// #define SERIAL_DEBUG 1


//------------------------------------------------------------------------------
// variables
//------------------------------------------------------------------------------
unsigned long ticks;
int tickState;

unsigned long usecPerTick;
static unsigned long usec;
static unsigned long nextTickUsec;

int tickID;
int tickCount[ NUM_TICKS ] = {  1000,   0,   0,   0,   0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0 };
int tickMax  [ NUM_TICKS ] = {     0, 480, 240, 160, 120, 96, 80, 68, 60, 53, 48, 43, 40, 36, 34, 32, 30 };

int switchState;

float beatIndexRecip;
float usecRecip;

float potLeft;
float potCenter;
float potRight;
float potFront;


//------------------------------------------------------------------------------
// channel class
//------------------------------------------------------------------------------
class channel
{
    public:
   
        // constructor
        channel( int relayPinInit, int audioPinInit, int ledPinInit )
        : relayState( LOW          )
        , relayPin  ( relayPinInit )
        , audioPin  ( audioPinInit )
        , ledPin    ( ledPinInit   )
        , tick      ( 0            )
        , a         ( NULL         )
        , b         ( NULL         )
        {}
   
        // variables
        int      relayState;
        int      beatIndex;
        int      relayPin;
        int      audioPin;
        int      ledPin;
        int      tick;
        channel* a;
        channel* b;
   
        // adds other channels to this channel
        void AddChannels( channel* channelA, channel* channelB )
        {
            // store pointers to other channels
            a = channelA;
            b = channelB;
        }
       
        // updates channel states once per tick
        void UpdateChannel()
        {
            // toggle the led once per beat
            if( tickCount[ beatIndex ] < LED_TICKS )
            {
                // turn led on
                digitalWrite( ledPin, HIGH );
            }
            else
            {
                // turn led off
                digitalWrite( ledPin, LOW );
            }

            // if tick count has been reset
            if( tickCount[ beatIndex ] == 0 )
            {
                switch( switchState )
                {
                    // this channel --------------------------------------------
                    case -1 :   // flip the relay state
                                relayState = !( relayState );
                               
                                // toggle the relay
                                digitalWrite( relayPin, relayState );
                       
                                // toggle the audio jack
                                digitalWrite( audioPin, relayState );

                                break;

                    // no channels ---------------------------------------------
                    case  0 :   delayMicroseconds( 10 );
                       
                                break;

                    // other channels ------------------------------------------
                    case  1 :   // update the "a" channel if it hasn't
                                // happened yet during this tick
                                if( a->tick != tickID )
                                {
                                    // update the current tick id
                                    a->tick = tickID;

                                    // flip the relay state
                                    a->relayState = !( a->relayState );
                                   
                                    // toggle the relay
                                    digitalWrite( a->relayPin, a->relayState );
                                   
                                    // toggle the audio jack
                                    digitalWrite( a->audioPin, a->relayState );
                                }
                       
                                // update the "b" channel if it hasn't
                                // happened yet during this tick
                                if( b->tick != tickID )
                                {
                                    // update the current tick id
                                    b->tick = tickID;
                                   
                                    // flip the relay state
                                    b->relayState = !( b->relayState );
                                   
                                    // toggle the relay
                                    digitalWrite( b->relayPin, b->relayState );
                                   
                                    // toggle the audio jack
                                    digitalWrite( b->audioPin, b->relayState );
                                }

                                break;
                }
            }
        }
};


//------------------------------------------------------------------------------
// objects
//------------------------------------------------------------------------------
static channel* left   = new channel( RELAY_PIN_LEFT,   AUDIO_PIN_LEFT,   LED_PIN_LEFT   );
static channel* center = new channel( RELAY_PIN_CENTER, AUDIO_PIN_CENTER, LED_PIN_CENTER );
static channel* right  = new channel( RELAY_PIN_RIGHT,  AUDIO_PIN_RIGHT,  LED_PIN_RIGHT  );


//------------------------------------------------------------------------------
// prototypes
//------------------------------------------------------------------------------
void setup();
void loop();
inline void ClearLCD();


//------------------------------------------------------------------------------
// setup - initializes arduino
//------------------------------------------------------------------------------
void setup()
{
    // setup pins
    pinMode( SWITCH_PIN_A,     INPUT  );
    pinMode( SWITCH_PIN_B,     INPUT  );
    pinMode( RELAY_PIN_LEFT,   OUTPUT );
    pinMode( RELAY_PIN_CENTER, OUTPUT );
    pinMode( RELAY_PIN_RIGHT,  OUTPUT );
    pinMode( AUDIO_PIN_LEFT,   OUTPUT );
    pinMode( AUDIO_PIN_CENTER, OUTPUT );
    pinMode( AUDIO_PIN_RIGHT,  OUTPUT );
    pinMode( LED_PIN_LEFT,     OUTPUT );
    pinMode( LED_PIN_CENTER,   OUTPUT );
    pinMode( LED_PIN_RIGHT,    OUTPUT );
    pinMode( LED_PIN_FRONT,    OUTPUT );

    // initialize pin states
    digitalWrite( LED_PIN_LEFT,     LOW );
    digitalWrite( LED_PIN_CENTER,   LOW );
    digitalWrite( LED_PIN_RIGHT,    LOW );
    digitalWrite( LED_PIN_FRONT,    LOW );
    digitalWrite( RELAY_PIN_LEFT,   LOW );
    digitalWrite( RELAY_PIN_CENTER, LOW );
    digitalWrite( RELAY_PIN_RIGHT,  LOW );
    digitalWrite( AUDIO_PIN_LEFT,   LOW );
    digitalWrite( AUDIO_PIN_CENTER, LOW );
    digitalWrite( AUDIO_PIN_RIGHT,  LOW );
   
    // initialize tick variables
    usecPerTick  = 0;
    usec         = 0;
    nextTickUsec = 0;
    tickState    = HIGH;
    tickID       = 0;
   
    // calculate intermediate reciprocals
    beatIndexRecip = ( 1.0 / 1024.0 ) * NUM_TICKS;
    usecRecip = 32.063477; // ( 33333 - 500 ) / 1024

    // set up channels
    left->AddChannels  ( center, right  );
    center->AddChannels( left,   right  );
    right->AddChannels ( left,   center );
   
    // initialize potentiometer values
    potLeft   = 1;
    potCenter = 1;
    potFront  = 1;
    potRight  = 1;
   
    // run the hello sequence
    digitalWrite( LED_PIN_LEFT, HIGH );
    digitalWrite( RELAY_PIN_LEFT, HIGH );
    delay( 100 );
   
    digitalWrite( LED_PIN_CENTER, HIGH );
    digitalWrite( RELAY_PIN_CENTER, HIGH );
    delay( 100 );
   
    digitalWrite( LED_PIN_RIGHT, HIGH );
    digitalWrite( RELAY_PIN_RIGHT, HIGH );
    delay( 100 );

    digitalWrite( LED_PIN_FRONT, HIGH );
    delay( 100 );
   
    digitalWrite( LED_PIN_LEFT, LOW );
    digitalWrite( RELAY_PIN_LEFT, LOW );
    delay( 100 );
   
    digitalWrite( LED_PIN_CENTER, LOW );
    digitalWrite( RELAY_PIN_CENTER, LOW );
    delay( 100 );
   
    digitalWrite( LED_PIN_RIGHT, LOW );
    digitalWrite( RELAY_PIN_RIGHT, LOW );
    delay( 100 );

    digitalWrite( LED_PIN_FRONT, LOW );
    delay( 100 );

    delay( 200 );

#ifdef SERIAL_DEBUG
    Serial.begin( 9600 );
#endif
}


//------------------------------------------------------------------------------
// loop - the main program loop
//------------------------------------------------------------------------------
void loop()                        
{
#ifdef SERIAL_DEBUG
    ClearLCD();
    Serial.print( usecPerTick );
#endif

    // read and lowpass filter the pot values
    potLeft   = ( analogRead( POT_PIN_LEFT   ) * 0.1 ) + ( potLeft   * 0.9 );
    potCenter = ( analogRead( POT_PIN_CENTER ) * 0.1 ) + ( potCenter * 0.9 );
    potRight  = ( analogRead( POT_PIN_RIGHT  ) * 0.1 ) + ( potRight  * 0.9 );
    potFront  = ( analogRead( POT_PIN_FRONT  ) * 0.1 ) + ( potFront  * 0.9 );

    // store tick indices based on scaled top pot values
    left->beatIndex   = potLeft   * beatIndexRecip;
    center->beatIndex = potCenter * beatIndexRecip;
    right->beatIndex  = potRight  * beatIndexRecip;
   
    // calculate usec per tick value based on scaled front pot value
    usecPerTick = potFront * usecRecip + 500;

    // read front panel switch state
    switchState = digitalRead( SWITCH_PIN_A ) + ( digitalRead( SWITCH_PIN_B ) * -1 );

    // check elapsed microseconds
    usec = micros();

    // tick if enough microseconds have elapsed
    if( ( long )( usec - nextTickUsec ) >= 0 )
    {
        // reset tick timer for next tick
        nextTickUsec = usec + usecPerTick;

        // increment tick id value for this tick
        ++tickID;

        // update front led
        if( tickCount[ 1 ] < LED_TICKS )
        {
            digitalWrite( LED_PIN_FRONT, HIGH );
        }
        else
        {
            digitalWrite( LED_PIN_FRONT, LOW );
        }

        // lock subdivisions to the "whole" beat
        if( tickCount[  1 ] == 0 )
        {
            tickCount[  2 ] = 0;
            tickCount[  3 ] = 0;
            tickCount[  4 ] = 0;
            tickCount[  5 ] = 0;
            tickCount[  6 ] = 0;
            tickCount[  7 ] = 0;
            tickCount[  8 ] = 0;
            tickCount[  9 ] = 0;
            tickCount[ 10 ] = 0;
            tickCount[ 11 ] = 0;
            tickCount[ 12 ] = 0;
            tickCount[ 13 ] = 0;
            tickCount[ 14 ] = 0;
            tickCount[ 15 ] = 0;
            tickCount[ 16 ] = 0;
        }

        // increment and wrap tick counts
        ( ( ++tickCount[  1 ] ) >= tickMax[  1 ] ) ? ( tickCount[  1 ] = 0 ) : NULL;
        ( ( ++tickCount[  2 ] ) >= tickMax[  2 ] ) ? ( tickCount[  2 ] = 0 ) : NULL;
        ( ( ++tickCount[  3 ] ) >= tickMax[  3 ] ) ? ( tickCount[  3 ] = 0 ) : NULL;
        ( ( ++tickCount[  4 ] ) >= tickMax[  4 ] ) ? ( tickCount[  4 ] = 0 ) : NULL;
        ( ( ++tickCount[  5 ] ) >= tickMax[  5 ] ) ? ( tickCount[  5 ] = 0 ) : NULL;
        ( ( ++tickCount[  6 ] ) >= tickMax[  6 ] ) ? ( tickCount[  6 ] = 0 ) : NULL;
        ( ( ++tickCount[  7 ] ) >= tickMax[  7 ] ) ? ( tickCount[  7 ] = 0 ) : NULL;
        ( ( ++tickCount[  8 ] ) >= tickMax[  8 ] ) ? ( tickCount[  8 ] = 0 ) : NULL;
        ( ( ++tickCount[  9 ] ) >= tickMax[  9 ] ) ? ( tickCount[  9 ] = 0 ) : NULL;
        ( ( ++tickCount[ 10 ] ) >= tickMax[ 10 ] ) ? ( tickCount[ 10 ] = 0 ) : NULL;
        ( ( ++tickCount[ 11 ] ) >= tickMax[ 11 ] ) ? ( tickCount[ 11 ] = 0 ) : NULL;
        ( ( ++tickCount[ 12 ] ) >= tickMax[ 12 ] ) ? ( tickCount[ 12 ] = 0 ) : NULL;
        ( ( ++tickCount[ 13 ] ) >= tickMax[ 13 ] ) ? ( tickCount[ 13 ] = 0 ) : NULL;
        ( ( ++tickCount[ 14 ] ) >= tickMax[ 14 ] ) ? ( tickCount[ 14 ] = 0 ) : NULL;
        ( ( ++tickCount[ 15 ] ) >= tickMax[ 15 ] ) ? ( tickCount[ 15 ] = 0 ) : NULL;
        ( ( ++tickCount[ 16 ] ) >= tickMax[ 16 ] ) ? ( tickCount[ 16 ] = 0 ) : NULL;

        // update each channel
        left  ->UpdateChannel();
        center->UpdateChannel();
        right ->UpdateChannel();
    }
}


//------------------------------------------------------------------------------
// ClearLCD - sends clear codes for spark fun serial lcd backpack
//------------------------------------------------------------------------------
inline void ClearLCD()
{
    Serial.write( 0xFE );
    Serial.write( 0x01 );
}


//------------------------------------------------------------------------------
// EOF
//------------------------------------------------------------------------------
 


Coil Winder
The relays used in this piece were purchased from a salvage store, and were originally wound with about fifty turns of very thick wire, requiring high voltage and high current to make them actuate. Apparently they were intended for use in power transmission facilities and designed to run directly from mains power. Energizing the relays with ten amps of mains power seemed quite foolish, so I decided to re-wind the coils and make the relays stay open with about nine volts and a fifth of an amp of current. This meant approximately 1000 turns of thinner magnet wire, and since I wanted them to look nice I decided to build a coil winding machine that could turn the spools while I guided the wire by hand. It has a foot pedal and knob to control speed, a small display to count turns, and can run forwards or backwards.

Coil Winder Videos
Coil Winder


Coil Winding Timelapse


Coil Winder Code
//------------------------------------------------------------------------------
//  main.ino
//  Coil Winder - Arduino
//
//  Created by Cooper Baker on 3/23/12.
//  Copyright (c) 2012 Cooper Baker. All rights reserved.
//------------------------------------------------------------------------------

#include "main.h"

//------------------------------------------------------------------------------
// definitions
//------------------------------------------------------------------------------
#define STEPS_PER_TURN      400

// digital pins
#define POLOLU_STEP_PIN     2
#define POLOLU_DIR_PIN      3
#define RESET_PIN           4
#define FORWARD_PIN         5
#define REVERSE_PIN         6
#define V_OUT               8
#define LED_PIN             13

// analog pin
#define POTENTIOMETER_PIN   0


//------------------------------------------------------------------------------
// prototypes
//------------------------------------------------------------------------------
void        setup();
void        loop();
inline void UpdateLCD();
void        SplashLCD();


//------------------------------------------------------------------------------
// variables
//------------------------------------------------------------------------------
int             runSwitch;
int             lastRunSwitch;

float           rpm;
float           rpmCoeff;

int             potentiometer;

long long       stepCount;
long            turnCount;

unsigned long   usec;
unsigned long   stepUsec;
unsigned long   lastUsec;

unsigned long   usecPerStep;
unsigned long   lastUsecPerStep;
unsigned long   usecTable[ 13 ] = { 1000000000, 150000, 30000, 15000, 10000, 7500, 6000, 5000, 4286, 3750, 3333, 3000, 3000 };

//------------------------------------------------------------------------------
// setup - initializes arduino and chip
//------------------------------------------------------------------------------
void setup()
{
    // start serial i/o
    Serial.begin( 38400 );

    // setup output pins
    pinMode( POLOLU_STEP_PIN, OUTPUT );
    pinMode( POLOLU_DIR_PIN,  OUTPUT );
    pinMode( V_OUT,           OUTPUT );
    pinMode( LED_PIN,         OUTPUT );

    // setup input pins
    pinMode( FORWARD_PIN,     INPUT );
    pinMode( REVERSE_PIN,     INPUT );
    pinMode( RESET_PIN,       INPUT );

    digitalWrite( V_OUT, HIGH );
   
    // init variables
    usec            = micros();
    lastUsec        = usec;
    stepUsec        = usec;

    usecPerStep     = 3000;
    lastUsecPerStep = usecPerStep;
   
    runSwitch       = 0;
    lastRunSwitch   = runSwitch;

    stepCount       = 0;
    turnCount       = 0;

    potentiometer   = 0;
   
    rpm             = 0;
    rpmCoeff        = 0.000001 * STEPS_PER_TURN;

    // display splash screen
    SplashLCD();

    // read interface pins and set up machine state
    runSwitch       = digitalRead( FORWARD_PIN ) + digitalRead( REVERSE_PIN ) * -1;
    potentiometer   = ( int )( ( ( float )analogRead( POTENTIOMETER_PIN ) / 1024.0 ) * 12 );
    usecPerStep     = usecTable[ potentiometer ];
    rpm             = ( 1.0 / ( usecPerStep * rpmCoeff ) ) * 60;
   
    // update lcd display
    UpdateLCD();
}


//------------------------------------------------------------------------------
// loop - the main program loop
//------------------------------------------------------------------------------
void loop()                        
{
    // direction switch
    //-------------------------------------------------------------------------
    lastRunSwitch = runSwitch;
    runSwitch = digitalRead( FORWARD_PIN ) + digitalRead( REVERSE_PIN ) * -1;
   
    // update if switch has changed position
    while( lastRunSwitch != runSwitch )
    {
        lastRunSwitch = runSwitch;
        runSwitch = digitalRead( FORWARD_PIN ) + digitalRead( REVERSE_PIN ) * -1;

        UpdateLCD();
    }
   
    // potentiometer
    //--------------------------------------------------------------------------
    lastUsecPerStep = usecPerStep;
    potentiometer = ( int )( ( ( float )analogRead( POTENTIOMETER_PIN ) / 1024.0 ) * 12 );
    usecPerStep = usecTable[ potentiometer ];

    // update if potentiometer has changed position
    while( lastUsecPerStep != usecPerStep )
    {
        lastUsecPerStep = usecPerStep;
        potentiometer = ( int )( ( ( float )analogRead( POTENTIOMETER_PIN ) / 1024.0 ) * 12 );
        usecPerStep = usecTable[ potentiometer ];

        // calculate rpm
        rpm = ( 1.0 / ( usecPerStep * rpmCoeff ) ) * 60;

        UpdateLCD();
    }
   
    // reset button
    //--------------------------------------------------------------------------
    while( digitalRead( RESET_PIN ) )
    {
        stepCount = 0;
        turnCount = 0;
       
        UpdateLCD();
    }

    // get current time (microsecond count)
    usec = micros();

    // schedule next step
    stepUsec = lastUsec + usecPerStep;

    // stepper update
    //--------------------------------------------------------------------------
    if( ( long )( usec - stepUsec ) >= 0 )
    {
        // remember this step time
        lastUsec = usec;

        // schedule next step
        stepUsec = lastUsec + usecPerStep;
       
        // if running
        if( runSwitch )
        {
            // set step direction
            digitalWrite( POLOLU_DIR_PIN, runSwitch + 1 );
            delayMicroseconds( 1 );
           
            // pulse the step pin
            digitalWrite( POLOLU_STEP_PIN, LOW );
            delayMicroseconds( 1 );
            digitalWrite( POLOLU_STEP_PIN, HIGH );
           
            // increment step count
            stepCount += runSwitch;

            // look for a completed turn
            if( ( stepCount % STEPS_PER_TURN ) == 0 )
            {
                // increment the turn count
                turnCount += runSwitch;

                // update display
                UpdateLCD();
            }
        }
    }
}


//------------------------------------------------------------------------------
// UpdateLCD - updates info display on the lcd
//------------------------------------------------------------------------------
inline void UpdateLCD()
{
    // clear lcd
    Serial.write( 0xFE );
    Serial.write( 0x01 );

    // display turns
    Serial.print( "TURNS " );
    Serial.print( turnCount );

    // move to line 2
    Serial.write( 0xFE );
    Serial.write( 0xC0 );    

    // display rpm info
    Serial.print( " RPM  " );
    Serial.print( rpm, 0 );

    // move to switch indicator position
    Serial.write( 0xFE );
    Serial.write( 0xCC );
   
    // display switch state
    switch( runSwitch )
    {
        case  1: Serial.print( "FWD" );
                 break;
       
        case -1: Serial.print( "REV" );
                 break;

        case  0: Serial.print( "STOP" );
                 break;
    }
}


//------------------------------------------------------------------------------
// SplashLCD - draws the animated splash screen
//------------------------------------------------------------------------------
void SplashLCD()
{
    const char* line1 = "Coilmatic v1.618";
    const char* line2 = "Cooper Baker '12";
   
    int i;

    // clear lcd
    Serial.write( 0xFE );
    Serial.write( 0x01 );
   
    // animated draw
    for( i = 0 ; i < 16 ; ++i )
    {
        Serial.write( 0xFE );
        Serial.write( 128 + i );
   
        Serial.print( line1[ i ] );

        Serial.write( 0xFE );
        Serial.write( 192 + ( 15 - i ) );

        Serial.print( line2[ 15 - i ] );

        delay( 66 );
    }
   
    delay( 1500 );

    // animated erase
    for( i = 0 ; i < 16 ; ++i )
    {
        Serial.write( 0xFE );
        Serial.write( 128 + i );
   
        Serial.print( " " );

        Serial.write( 0xFE );
        Serial.write( 192 + ( 15 - i ) );

        Serial.print( " " );

        delay( 66 );
    }
}


//------------------------------------------------------------------------------
// EOF
//------------------------------------------------------------------------------